Um guia completo sobre genéricos TypeScript, cobrindo sua sintaxe, benefícios, uso avançado e melhores práticas para lidar com tipos de dados complexos no desenvolvimento de software global.
Genéricos TypeScript: Dominando Tipos de Dados Complexos para Aplicações Robustas
TypeScript, um superconjunto do JavaScript, capacita os desenvolvedores a escrever código mais robusto e de fácil manutenção por meio da tipagem estática. Entre seus recursos mais poderosos estão os genéricos, que permitem escrever código que pode funcionar com uma variedade de tipos de dados, mantendo a segurança de tipos. Este guia oferece uma exploração abrangente dos genéricos TypeScript, focando em sua aplicação a tipos de dados complexos no contexto do desenvolvimento de software global.
O que são Genéricos?
Genéricos fornecem uma maneira de escrever código reutilizável que pode funcionar com diferentes tipos. Em vez de escrever funções ou classes separadas para cada tipo que você deseja suportar, você pode escrever uma única função ou classe que usa parâmetros de tipo. Esses parâmetros de tipo são espaços reservados para os tipos reais que serão usados quando a função ou classe for chamada ou instanciada. Isso é especialmente útil ao lidar com estruturas de dados complexas onde o tipo de dados dentro dessas estruturas pode variar.
Benefícios de Usar Genéricos
- Reutilização de Código: Escreva código uma vez e use-o com diferentes tipos. Isso reduz a duplicação de código e torna sua base de código mais fácil de manter.
- Segurança de Tipos: Os genéricos permitem que o compilador TypeScript imponha a segurança de tipos em tempo de compilação. Isso ajuda a prevenir erros em tempo de execução relacionados a incompatibilidades de tipos.
- Legibilidade Aprimorada: Os genéricos tornam seu código mais legível, indicando claramente os tipos com os quais suas funções e classes são projetadas para trabalhar.
- Desempenho Aprimorado: Em alguns casos, os genéricos podem levar a melhorias de desempenho porque o compilador pode otimizar o código gerado com base nos tipos específicos que estão sendo usados.
Sintaxe Básica de Genéricos
A sintaxe básica de genéricos envolve o uso de colchetes angulares (< >) para declarar parâmetros de tipo. Esses parâmetros de tipo são tipicamente nomeados T
, K
, V
, etc., mas você pode usar qualquer identificador válido. Aqui está um exemplo simples de uma função genérica:
function identity<T>(arg: T): T {
return arg;
}
let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
let myBoolean: boolean = identity<boolean>(true);
console.log(myString); // Saída: hello
console.log(myNumber); // Saída: 123
console.log(myBoolean); // Saída: true
Neste exemplo, <T>
declara um parâmetro de tipo chamado T
. A função identity
recebe um argumento do tipo T
e retorna um valor do tipo T
. Ao chamar a função, você pode especificar explicitamente o parâmetro de tipo (por exemplo, identity<string>
) ou deixar o TypeScript inferi-lo com base no tipo do argumento.
Trabalhando com Tipos de Dados Complexos
Genéricos se tornam particularmente valiosos ao lidar com tipos de dados complexos como arrays, objetos e interfaces. Vamos explorar alguns cenários comuns:
Arrays Genéricos
Você pode usar genéricos para criar funções ou classes que funcionam com arrays de diferentes tipos:
function arrayToString<T>(arr: T[]): string {
return arr.join(", ");
}
let numberArray: number[] = [1, 2, 3, 4, 5];
let stringArray: string[] = ["apple", "banana", "cherry"];
console.log(arrayToString(numberArray)); // Saída: 1, 2, 3, 4, 5
console.log(arrayToString(stringArray)); // Saída: apple, banana, cherry
Aqui, a função arrayToString
recebe um array do tipo T[]
e retorna uma representação em string do array. Esta função funciona com arrays de qualquer tipo, tornando-a altamente reutilizável.
Objetos Genéricos
Genéricos também podem ser usados para definir funções ou classes que trabalham com objetos de diferentes formatos:
interface Person {
name: string;
age: number;
country: string; // Adicionado país para contexto global
}
interface Product {
id: number;
name: string;
price: number;
currency: string; // Adicionada moeda para contexto global
}
function displayInfo<T extends { name: string }>(item: T): void {
console.log(`Name: ${item.name}`);
}
let person: Person = { name: "Alice", age: 30, country: "USA" };
let product: Product = { id: 1, name: "Laptop", price: 1200, currency: "USD" };
displayInfo(person); // Saída: Name: Alice
displayInfo(product); // Saída: Name: Laptop
Neste exemplo, a função displayInfo
recebe um objeto do tipo T
que deve ter uma propriedade name
do tipo string. A cláusula extends { name: string }
é uma restrição (constraint), que especifica os requisitos mínimos para o parâmetro de tipo T
. Isso garante que a função possa acessar com segurança a propriedade name
.
Uso Avançado de Genéricos
Os genéricos do TypeScript oferecem recursos mais avançados que permitem criar código ainda mais flexível e poderoso. Vamos explorar alguns desses recursos:
Múltiplos Parâmetros de Tipo
Você pode definir funções ou classes com múltiplos parâmetros de tipo:
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
interface Name {
firstName: string;
}
interface Age {
age: number;
}
const person: Name = { firstName: "Bob" };
const details: Age = { age: 42 };
const merged = merge(person, details);
console.log(merged.firstName); // Saída: Bob
console.log(merged.age); // Saída: 42
A função merge
recebe dois objetos dos tipos T
e U
e retorna um novo objeto que contém as propriedades de ambos os objetos. Esta é uma maneira poderosa de combinar dados de diferentes fontes.
Restrições Genéricas
Como mostrado anteriormente, as restrições permitem que você restrinja os tipos que podem ser usados com um parâmetro de tipo genérico. Isso garante que o código genérico possa operar com segurança nos tipos especificados.
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity([1, 2, 3]); // Saída: 3
loggingIdentity("hello"); // Saída: 5
// loggingIdentity(123); // Erro: O argumento do tipo 'number' não é atribuível ao parâmetro do tipo 'Lengthwise'.
A função loggingIdentity
recebe um argumento do tipo T
que deve ter uma propriedade length
do tipo number. Isso garante que a função possa acessar com segurança a propriedade length
.
Classes Genéricas
Genéricos também podem ser usados com classes:
class DataStorage<T> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
this.data = this.data.filter(d => d !== item);
}
getItems(): T[] {
return [...this.data];
}
}
const textStorage = new DataStorage<string>();
textStorage.addItem("apple");
textStorage.addItem("banana");
textStorage.removeItem("apple");
console.log(textStorage.getItems()); // Saída: [ 'banana' ]
const numberStorage = new DataStorage<number>();
numberStorage.addItem(1);
numberStorage.addItem(2);
numberStorage.removeItem(1);
console.log(numberStorage.getItems()); // Saída: [ 2 ]
A classe DataStorage
pode armazenar dados de qualquer tipo T
. Isso permite que você crie estruturas de dados reutilizáveis que são seguras quanto ao tipo.
Interfaces Genéricas
Interfaces genéricas são úteis para definir contratos que podem funcionar com diferentes tipos. Por exemplo:
interface Result<T, E> {
success: boolean;
data?: T;
error?: E;
}
interface User {
id: number;
username: string;
email: string;
}
interface ErrorMessage {
code: number;
message: string;
}
function fetchUser(id: number): Result<User, ErrorMessage> {
if (id === 1) {
return { success: true, data: { id: 1, username: "john.doe", email: "john.doe@example.com" } };
} else {
return { success: false, error: { code: 404, message: "User not found" } };
}
}
const userResult = fetchUser(1);
if (userResult.success) {
console.log(userResult.data.username);
} else {
console.log(userResult.error.message);
}
A interface Result
define uma estrutura genérica para representar o resultado de uma operação. Ela pode conter dados do tipo T
ou um erro do tipo E
. Este é um padrão comum para lidar com operações assíncronas ou operações que podem falhar.
Tipos Utilitários e Genéricos
O TypeScript fornece vários tipos utilitários integrados que funcionam bem com genéricos. Esses tipos utilitários podem ajudá-lo a transformar e manipular tipos de maneiras poderosas.
Partial<T>
Partial<T>
torna todas as propriedades do tipo T
opcionais:
interface Person {
name: string;
age: number;
}
type PartialPerson = Partial<Person>;
const partialPerson: PartialPerson = { name: "Alice" }; // Válido
Readonly<T>
Readonly<T>
torna todas as propriedades do tipo T
somente leitura:
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = Readonly<Person>;
const readonlyPerson: ReadonlyPerson = { name: "Bob", age: 42 };
// readonlyPerson.age = 43; // Erro: Não é possível atribuir a 'age' porque é uma propriedade somente leitura.
Pick<T, K>
Pick<T, K>
seleciona um conjunto de propriedades K
do tipo T
:
interface Person {
name: string;
age: number;
email: string;
}
type NameAndAge = Pick<Person, "name" | "age">;
const nameAndAge: NameAndAge = { name: "Charlie", age: 28 };
Omit<T, K>
Omit<T, K>
remove um conjunto de propriedades K
do tipo T
:
interface Person {
name: string;
age: number;
email: string;
}
type PersonWithoutEmail = Omit<Person, "email">;
const personWithoutEmail: PersonWithoutEmail = { name: "David", age: 35 };
Record<K, T>
Record<K, T>
cria um tipo com chaves K
e valores do tipo T
:
type CountryCodes = "US" | "CA" | "UK" | "DE" | "FR" | "JP" | "CN" | "IN" | "BR" | "AU"; // Lista expandida para contexto global
type Currency = "USD" | "CAD" | "GBP" | "EUR" | "JPY" | "CNY" | "INR" | "BRL" | "AUD"; // Lista expandida para contexto global
type CurrencyMap = Record<CountryCodes, Currency>;
const currencyMap: CurrencyMap = {
"US": "USD",
"CA": "CAD",
"UK": "GBP",
"DE": "EUR",
"FR": "EUR",
"JP": "JPY",
"CN": "CNY",
"IN": "INR",
"BR": "BRL",
"AU": "AUD",
};
Tipos Mapeados
Tipos mapeados permitem que você transforme tipos existentes iterando sobre suas propriedades. Esta é uma maneira poderosa de criar novos tipos com base nos existentes. Por exemplo, você pode criar um tipo que torna todas as propriedades de outro tipo somente leitura:
interface Person {
name: string;
age: number;
}
type ReadonlyPerson = {
readonly [K in keyof Person]: Person[K];
};
const readonlyPerson: ReadonlyPerson = { name: "Eve", age: 25 };
// readonlyPerson.age = 26; // Erro: Não é possível atribuir a 'age' porque é uma propriedade somente leitura.
Neste exemplo, [K in keyof Person]
itera sobre todas as chaves da interface Person
, e Person[K]
acessa o tipo de cada propriedade. A palavra-chave readonly
torna cada propriedade somente leitura.
Tipos Condicionais
Tipos condicionais permitem que você defina tipos com base em condições. Esta é uma maneira poderosa de criar tipos que se adaptam a diferentes cenários.
type NonNullable<T> = T extends null | undefined ? never : T;
type MaybeString = string | null | undefined;
type StringType = NonNullable<MaybeString>; // string
function getValue<T>(value: T): NonNullable<T> {
if (value == null) { // Lida com null e undefined
throw new Error("O valor não pode ser nulo ou indefinido");
}
return value as NonNullable<T>;
}
try {
const validValue = getValue("hello");
console.log(validValue.toUpperCase()); // Saída: HELLO
const invalidValue = getValue(null); // Isso irá lançar um erro
console.log(invalidValue); // Esta linha não será alcançada
} catch (error: any) {
console.error(error.message); // Saída: O valor não pode ser nulo ou indefinido
}
Neste exemplo, o tipo NonNullable<T>
verifica se T
é null
ou undefined
. Se for, ele retorna never
, o que significa que o tipo não é permitido. Caso contrário, ele retorna T
. Isso permite criar tipos que são garantidamente não nulos.
Melhores Práticas para Usar Genéricos
Aqui estão algumas melhores práticas a serem lembradas ao usar genéricos:
- Use nomes de parâmetros de tipo descritivos: Escolha nomes que indiquem claramente o propósito do parâmetro de tipo.
- Use restrições para limitar os tipos que podem ser usados com um parâmetro de tipo genérico: Isso garante que seu código genérico possa operar com segurança nos tipos especificados.
- Mantenha seu código genérico simples e focado: Evite complicar demais seu código genérico com muitos parâmetros de tipo ou restrições complexas.
- Documente seu código genérico completamente: Explique o propósito dos parâmetros de tipo e quaisquer restrições que sejam usadas.
- Considere os prós e contras entre a reutilização de código e a segurança de tipos: Embora os genéricos possam melhorar a reutilização de código, eles também podem tornar seu código mais complexo. Pese os benefícios e desvantagens antes de usar genéricos.
- Considere a localização e a globalização (l10n e g11n): Ao lidar com dados que precisam ser exibidos para usuários em diferentes regiões, garanta que seus genéricos suportem formatação e convenções culturais apropriadas. Por exemplo, a formatação de números e datas pode variar significativamente entre localidades.
Exemplos em um Contexto Global
Vamos considerar alguns exemplos de como os genéricos podem ser usados em um contexto global:
Conversão de Moeda
interface ConversionRate {
rate: number;
fromCurrency: string;
toCurrency: string;
}
function convertCurrency<T extends ConversionRate>(amount: number, rate: T): number {
return amount * rate.rate;
}
const usdToEurRate: ConversionRate = { rate: 0.85, fromCurrency: "USD", toCurrency: "EUR" };
const amountInUSD = 100;
const amountInEUR = convertCurrency(amountInUSD, usdToEurRate);
console.log(`${amountInUSD} USD é igual a ${amountInEUR} EUR`); // Saída: 100 USD é igual a 85 EUR
Formatação de Data
interface DateFormatOptions {
locale: string;
options: Intl.DateTimeFormatOptions;
}
function formatDate<T extends DateFormatOptions>(date: Date, format: T): string {
return date.toLocaleDateString(format.locale, format.options);
}
const currentDate = new Date();
const usDateFormat: DateFormatOptions = { locale: "en-US", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const germanDateFormat: DateFormatOptions = { locale: "de-DE", options: { year: 'numeric', month: 'long', day: 'numeric' } };
const japaneseDateFormat: DateFormatOptions = { locale: "ja-JP", options: { year: 'numeric', month: 'long', day: 'numeric' } };
console.log("Data EUA: " + formatDate(currentDate, usDateFormat));
console.log("Data Alemã: " + formatDate(currentDate, germanDateFormat));
console.log("Data Japonesa: " + formatDate(currentDate, japaneseDateFormat));
Serviço de Tradução
interface Translation {
[key: string]: string; // Permite chaves de idioma dinâmicas
}
interface LanguageData<T extends Translation> {
languageCode: string;
translations: T;
}
const englishTranslations: Translation = {
"hello": "Hello",
"goodbye": "Goodbye",
"welcome": "Welcome to our website!"
};
const spanishTranslations: Translation = {
"hello": "Hola",
"goodbye": "Adiós",
"welcome": "¡Bienvenido a nuestro sitio web!"
};
const frenchTranslations: Translation = {
"hello": "Bonjour",
"goodbye": "Au revoir",
"welcome": "Bienvenue sur notre site web !"
};
const languageData: LanguageData<typeof englishTranslations>[] = [
{languageCode: "en", translations: englishTranslations },
{languageCode: "es", translations: spanishTranslations },
{languageCode: "fr", translations: frenchTranslations}
];
function translate<T extends Translation>(key: string, languageCode: string, languageData: LanguageData<T>[]): string {
const lang = languageData.find(lang => lang.languageCode === languageCode);
if (!lang) {
return `Tradução para ${key} em ${languageCode} não encontrada.`;
}
return lang.translations[key] || `Tradução para ${key} não encontrada.`;
}
console.log(translate("hello", "en", languageData)); // Saída: Hello
console.log(translate("hello", "es", languageData)); // Saída: Hola
console.log(translate("welcome", "fr", languageData)); // Saída: Bienvenue sur notre site web !
console.log(translate("missingKey", "de", languageData)); // Saída: Tradução para missingKey em de não encontrada.
Conclusão
Os genéricos do TypeScript são uma ferramenta poderosa para escrever código reutilizável e seguro quanto ao tipo que pode trabalhar com tipos de dados complexos. Ao entender a sintaxe básica, os recursos avançados e as melhores práticas dos genéricos, você pode melhorar significativamente a qualidade e a capacidade de manutenção de suas aplicações TypeScript. Ao desenvolver aplicações para um público global, os genéricos podem ajudá-lo a lidar com diversos formatos de dados e convenções culturais, garantindo uma experiência de usuário perfeita para todos.